/**
 *
 */
package com.angel.annotation.managers;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.angel.annotation.exceptions.AnnotationManagerException;
import com.angel.annotation.finders.AnnotationClassFinder;
import com.angel.annotation.finders.AnnotationConstructorFinder;
import com.angel.annotation.finders.AnnotationFieldFinder;
import com.angel.annotation.finders.AnnotationMethodFinder;
import com.angel.annotation.finders.AnnotationPackageFinder;
import com.angel.annotation.interfaces.AnnotationAction;
import com.angel.annotation.interfaces.AnnotationObjectFinder;
import com.angel.common.helpers.AnnotationHelper;
import com.angel.common.helpers.ReflectionHelper;

/** This class manager a specific annotation.
 *
 * @author William
 *
 */
public abstract class AnnotationManager {

	/** Target actions' list for this annotation manager. It defines an action for each target.*/
	private List<AnnotationTargetAction> targetActions = new ArrayList<AnnotationTargetAction>();

	/** Define strategies to find a annotation implementation. */
	private List<AnnotationObjectFinder> annotationFinderStrategies = new ArrayList<AnnotationObjectFinder>();

	/** Test if it can process object with this annotation manager.
	 *
	 * @param object to process.
	 * @return true if it can process object. Otherwise it returns false.
	 */
	public abstract Boolean canProcessAnnotationAt(Object object);

	/** Get annotation class for this manager.
	 *
	 * @return an annotation class.
	 */
	public abstract Class<? extends Annotation> getClassAnnotation();

	public AnnotationManager() {
		super();
		annotationFinderStrategies.add(AnnotationClassFinder.getInstance());
		annotationFinderStrategies.add(AnnotationConstructorFinder.getInstance());
		annotationFinderStrategies.add(AnnotationFieldFinder.getInstance());
		annotationFinderStrategies.add(AnnotationMethodFinder.getInstance());
		annotationFinderStrategies.add(AnnotationPackageFinder.getInstance());
	}

	public AnnotationManager(Map<ElementType, AnnotationAction> targetActions) {
		this();
		this.addAllTargetActions(targetActions);
	}

	/** Get current annotation implementation for an object.
	 *
	 * @param object to get its annotation implementation.
	 * @return current annotation implementation.
	 */
	protected Annotation getAnnotationImpl(Object object){
		Annotation annotation = null;
		for(AnnotationObjectFinder aof: this.annotationFinderStrategies){
			annotation = aof.findAnnotationAt(object, this.getClassAnnotation());
			if(annotation != null){
				break;
			}
		}
		return annotation;
	}

	protected Object findTargetObjectFor(Object object){
		Object targetObject = null;
		for(AnnotationObjectFinder aof: this.annotationFinderStrategies){
			targetObject = aof.findTargetObjectFor(object, this.getClassAnnotation());
			if(targetObject != null){
				break;
			}
		}
		return targetObject;
	}

	protected boolean containsActionFor(ElementType target){
		for(AnnotationTargetAction targetAction: this.targetActions){
			if(targetAction.getTarget() == target){
				return true;
			}
		}
		return false;
	}

	protected boolean removeActionFor(ElementType target){
		boolean removed = false;
		if(this.containsActionFor(target)){
			AnnotationTargetAction targetAction = this.findAnnotationTargetActionFor(target);
			removed = this.targetActions.remove(targetAction);
		}
		return removed;
	}

	protected AnnotationTargetAction findAnnotationTargetActionFor(ElementType target){
		if(!this.containsActionFor(target)){
			throw new AnnotationManagerException("Annotation action with target [" + target.name() + "] was NOT found at annotation manager.");
		}
		for(AnnotationTargetAction targetAction: this.targetActions){
			if(targetAction.isForTarget(target)){
				return targetAction;
			}
		}
		return null;
	}

	protected boolean overridesAction(ElementType target, AnnotationAction action){
		boolean overrided = false;
		if(this.containsActionFor(target)){
			this.removeActionFor(target);
			this.addAction(target, action);
		}
		return overrided;
	}

	protected boolean addAllTargetActions(Map<ElementType,AnnotationAction> targetActions){
		boolean addedAll = true;
		for(ElementType target: targetActions.keySet()){
			addedAll = this.addAction(target, targetActions.get(target));
		}
		return addedAll;
	}

	protected boolean addAction(ElementType target, AnnotationAction action){
		boolean added = false;
		if(!this.containsActionFor(target)){
			added = this.targetActions.add(new AnnotationTargetAction(target, action));
		}
		return added;
	}

	protected Object getAnnotationValue(String property, Object object){
		Annotation annotation = this.getAnnotation(object);
		Method methodToInvoke = ReflectionHelper.getMethodWithoutParameters(annotation, property);
		if(methodToInvoke == null){
			throw new AnnotationManagerException("Cannot find property [" + property + "] at annotation [" + this.getClassAnnotation() + "].");
		}
		return ReflectionHelper.invokeMethodWithoutParameters(annotation, methodToInvoke);
	}

	public Annotation getAnnotation(Object object){
		Annotation annotation = null;
		if(this.canProcessAnnotationAt(object)){
			annotation = this.getAnnotationImpl(object);
		} else {
			throw new AnnotationManagerException("Annotation [" + this.getClassAnnotation() + "]cannot be found at object [" + object + "].");
		}
		return annotation;
	}

	protected Object processAnnotationTargetAction(Object object, AnnotationTargetAction targetAction, Object[] parameters){
		Object objectValue = null;
		if(this.canProcessAnnotationAt(object)){
			try{
				AnnotationAction action = targetAction.getAnnotationAction();
				Object targetObject = this.findTargetObjectFor(object);

				objectValue = action.doAction(this, object, targetObject, parameters);
			} catch (Exception e) {
				throw new AnnotationManagerException("An error occurred processing annotation [" + this.getClassAnnotation() + "] with object [" + object + "].", e);
			}
		} else {
			throw new AnnotationManagerException("Object class [" + object.getClass() + "] cannot be processed by annotation [" + this.getClassAnnotation() + "].");
		}
		return objectValue;
	}

	public Object processAnnotation(Object object, ElementType target, Object[] parameters){
		AnnotationTargetAction targetAction = this.findAnnotationTargetActionFor(target);
		if(targetAction == null){
			throw new AnnotationManagerException("Annotation action with target [" + target.name() + "] not found.");
		}
		return this.processAnnotationTargetAction(object, targetAction, parameters);
	}

	public Object processAnnotation(Object object){
		return this.processAnnotation(object, null);
	}

	public Object processAnnotation(Object object, Object[] parameters){
		AnnotationTargetAction defaultAction = this.findDefaultAnnotationTargetAction();
		return this.processAnnotationTargetAction(object, defaultAction, parameters);
	}

	protected AnnotationTargetAction findDefaultAnnotationTargetAction(){
		AnnotationTargetAction targetAction = null;
		for(AnnotationTargetAction ata: targetActions){
			if(targetAction == null && ata.isDefault()){
				targetAction = ata;
			}
		}
		if(targetAction == null){
			throw new AnnotationManagerException("There is not an annotation action by default.");
		}
		return targetAction;
	}

	/** Test if an object has only and only one annotation implementation at a annotation's collection.
	 *
	 * @param object to test its annotations.
	 * @param annotationsClass to verify in the object.
	 * @return true if object has only and only one annotation implementation. Otherwise it returns false.
	 */
	public Boolean hasOnlyOne(Object object, Class<? extends Annotation> ...annotationsClass){
		int quantity = 0;
		for(Class<? extends Annotation> annotationClass : annotationsClass){
			if(AnnotationHelper.hasAnnotation(object.getClass(), annotationClass)){
				quantity++;
			}
		}
		boolean hasOnlyOne = true;
		if(quantity > 1){
			hasOnlyOne = false;
		}
		return hasOnlyOne;
	}
}
